using System;
using System.Collections;
using System.IO;
using System.Threading;
using System.Web;
using System.Web.Caching;
using System.Xml.Serialization;

namespace Artem {

    /// <summary>
    /// Generic class for a perssitable object. 
    /// Encapsulates the logic to load/save data from/to the filesystem. 
    /// To speed up the acces, caching is used.
    /// </summary>
    /// <typeparam name="T">class or struct with all the data-fields that must be persisted</typeparam>
    public class Persistable<T> where T : new() {

        #region Static Fields ///////////////////////////////////////////////////////////

        public static readonly TimeSpan DefaultSlidingExpiration = TimeSpan.FromMinutes(30);

        #endregion

        #region Fields  /////////////////////////////////////////////////////////////////

        string _fileName;
        XmlSerializer _serializer;
        TimeSpan _slidingExpiration;
        [NonSerialized]
        object _syncRoot = new object();
        T _value;

        #endregion

        #region Properties  /////////////////////////////////////////////////////////////

        /// <summary>
        /// Gets a value indicating whether this instance is empty.
        /// </summary>
        /// <value><c>true</c> if this instance is empty; otherwise, <c>false</c>.</value>
        public bool IsEmpty {
            get {
                return (_value == null || (_value is ICollection && (_value as ICollection).Count == 0));
            }
        }

        /// <summary>
        /// Gets the sync root.
        /// </summary>
        /// <value>The sync root.</value>
        public virtual object SyncRoot {
            get { return _syncRoot; }
        }

        /// <summary>
        /// Gets or sets the data.
        /// </summary>
        /// <value>The data.</value>
        public virtual T Value {
            get {
                lock (SyncRoot) {
                    if (IsEmpty) Load();
                    else CachePing();
                    return _value;
                }
            }
            set {
                lock (SyncRoot) {
                    _value = value;
                }
            }
        }
        #endregion

        #region Construct  //////////////////////////////////////////////////////////////

        /// <summary>
        /// Creates a instance of Persistable. Also creates a instance of T
        /// </summary>
        protected Persistable(string fileName, TimeSpan cacheSlidingExpiration) {
            _fileName = fileName;
            _serializer = new XmlSerializer(typeof(T));
            _slidingExpiration = cacheSlidingExpiration;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="fileName"></param>
        protected Persistable(string fileName)
            : this(fileName, DefaultSlidingExpiration) {
        }
        #endregion

        #region Methods /////////////////////////////////////////////////////////////////

        /// <summary>
        /// Deletes the data from the cache and filesystem
        /// </summary>
        /// <returns></returns>
        public virtual bool Delete() {

            bool success = true;
            lock (SyncRoot) {
                if (File.Exists(_fileName)) {
                    try {
                        File.Delete(_fileName);
                    }
                    catch { success = false; }
                }
            }
            return success;
        }

        /// <summary>
        /// Loads the data from the filesystem. For deserialization a XmlSeralizer is used.
        /// </summary>
        public T Load() {

            lock (SyncRoot) {
                try {
                    if (System.IO.File.Exists(_fileName)) {
                        using (FileStream reader = File.Open(_fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) {
                            _value = (T)_serializer.Deserialize(reader);
                        }
                    }
                    AddFileNotifier();
                }
                catch (Exception ex) {
                    throw new Exception(
                        string.Format("Unable to load persitable object from file {0}", _fileName), ex);
                }
            }
            return _value;
        }

        /// <summary>
        /// Persists the data back to the filesystem
        /// </summary>
        /// <param name="fileName">Name of the file.</param>
        public void Save() {

            if (!IsEmpty) {
                lock (SyncRoot) {
                    try {
                        string directory = Path.GetDirectoryName(_fileName);
                        if (!Directory.Exists(directory))
                            Directory.CreateDirectory(directory);
                        T value = _value;
                        using (FileStream writer = File.Create(_fileName)) {
                            _serializer.Serialize(writer, value);
                        }
                    }
                    catch (Exception ex) {
                        throw new Exception(
                            string.Format("Unable to save persitable object to file {0}", _fileName), ex);
                    }
                }
            }
        }

        #region - Cache -

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        void AddFileNotifier() {
            HttpRuntime.Cache.Insert(
                _fileName,
                new object(),
                new CacheDependency(_fileName),
                Cache.NoAbsoluteExpiration,
                _slidingExpiration,
                CacheItemPriority.Normal,
                new CacheItemRemovedCallback(OnFileChanged));
        }

        /// <summary>
        /// Caches the ping.
        /// </summary>
        void CachePing() {
            object ping = HttpRuntime.Cache[_fileName];
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <param name="reason"></param>
        void OnFileChanged(string key, object value, CacheItemRemovedReason reason) {
            // invalidate value
            if (key == _fileName) _value = default(T);
        }
        #endregion
        #endregion
    }
}
